C#进阶(中) Aholic~茜 2025-07-09 2025-07-16 委托和事件 委托 委托是什么 委托是函数的容器,可以理解为表示函数的变量类型,用来储存、传递函数。 委托的本质是一个类,用来定义函数的类型,不同的函数必须对应和各自格式一致的委托。
语法 1 2 3 访问修饰符默认不写是public public delegate 返回值 委托名(参数列表);一般写在namespace 语句块中
定义 1 2 3 delegate void MyFun () ;public delegate int MyFun2 (int x ) ;
使用自定义委托 委托变量是函数的容器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 MyFun f = new MyFun(Fun); f.Invoke(); MyFun f2 = Fun; f2(); MyFun f3 = Fun2; int x = f3(114514 );static void Fun (){ } static int Fun2 (int x ){ return x; }
委托常用:作为类成员;作为函数的参数
1 2 3 4 5 6 7 8 9 10 11 12 13 class Test { public MyFun fun; public MyFun2 fun2; public void TestFun (MyFun fun, MyFun2 fun2 ) { fun(); fun2(114514 ); } } Test t = new Test(); t.TestFun(Fun, Fun2);
委托变量可以存储多个函数(多播委托) 1 2 3 4 5 6 7 8 9 10 MyFun ff = Fun; ff += Fun; ff(); ff -= Fun; ff();
系统定义好的委托 1 2 3 4 5 6 7 8 9 using System;Action Action action = Fun; Func<string > Func<int > func = Fun2; Action<int ,string ,...> act = Funx;
事件 事件是什么 事件是基于委托的存在,事件是委托的安全包裹,让委托的使用更有安全性。事件是一种特殊的变量类型。
事件的使用 事件是作为成员变量存在于类中的,委托怎么用事件就怎么用。但是事件不能在类外部调用,赋值。 注意,它只能作为成员存在于类和接口以及结构体中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Test { public Action myFun; public event Action myEvent; public void Test () { myFun = Fun; myEvent = Fun; } myEvent += Fun; muFun += Fun; }
虽然不能在外面赋值,但是可以进行加减来添加移除。只能在类的内部封装调用。
为什么有事件
防止外部随意置空委托
防止外部随意调用委托
事件相当于对委托进行了一次封装,使其更安全。
匿名函数 什么是匿名函数 没有名字的函数,匿名函数的使用主要是配合委托和事件使用,脱离委托和事件不会使用匿名函数
语法 使用关键字delegate修饰函数
何时使用:函数中传递委托参数时,委托或事件赋值时。
使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 Action a = delegate () { }; a(); Action<int , string > b = delegate (int a, string b) { } b(1145 ,"宇智波" ); Func<int > c = delegate () { return 1 ; } c(); Test t = new Test(); t.Dos(1145 , delegate () { Console.WriteLine(); } ) class Test { public Action action; public void Dos (int a, Action fun ) { fun(); } public Action GetFun () { return delegate () { }; } }
匿名函数的缺点 添加到委托或事件容器后,不记录,无法单独移除。因为匿名函数没有名字,在进行委托的加减的时候无法自由删除匿名函数。一般删除时,只能将委托清空。
因此我们在使用匿名函数的时候,一般都是在使用只有一个匿名函数的时候可以使用匿名函数,这样可以随时清空也不会移除其他函数。
Lambda表达式 什么是Lambda表达式 可以把lambda表达式理解为匿名函数的简写,除了写法不同以外,使用上和匿名函数一模一样,二者都是与委托和事件配合使用的。
Lambda表达式语法
Lambda表达式使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Action a = () => { Console.WriteLine("无参无返回值的表达式" ); }; a(); Action<int > b = (int a2) => { Console.WriteLine("有参无返回值的表达式" , a2); }; b(114514 ); Action<int > c = (a2) => { Console.WriteLine("有参无返回值的表达式" , a2); }; c(114514 ); Func<string , int > d = (value ) => { Console.WriteLine("有参数有返回值的表达式{0}" , value ); return 1 ; }; Console.WriteLine(d("宇智波" ));
其他使用上与匿名函数一样,包括缺点都与匿名函数一致。
闭包 内层的函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止了。注意:该变量提供的值并非变量创造时的值,而是在父函数氛围内的最终值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Test { public event Action action; public Test () { int value = 114514 ; action = () => { Console.WriteLine(value ); }; for (int i = 0 ; i < n; i++) { action += () => { Console.WriteLine(i); }; } } }
List排序 List自带的排序方法 1 2 3 4 5 6 7 8 9 List<int > list = new List<int >(); list.Add(1 ); list.Add(1 ); list.Add(4 ); list.Add(5 ); list.Add(1 ); list.Add(4 ); list.Sort();
自定义类的排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Item : IComparable <Item >{ public int money; public Item (int money ) { this .money = money; } public int CompareTo (Item other ) { if (this .money > other.money) { return 1 ; } else return -1 ; return 0 ; } } List<Item> itemlist = new List<Item>(); itemlist.Add(new Item(1 )); itemlist.Add(new Item(1 )); itemlist.Add(new Item(4 )); itemlist.Add(new Item(5 )); itemlist.Add(new Item(1 )); itemlist.Add(new Item(4 )); itemlist.Sort();
通过委托函数进行排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class ShopItem { public int id; public ShopItem (int id ) { this .id = id; } } List<ShopList>shoplist = new List<ShopList>(); shopitem.Add(new ShopItem(1 )); shopitem.Add(new ShopItem(1 )); shopitem.Add(new ShopItem(4 )); shopitem.Add(new ShopItem(5 )); shopitem.Add(new ShopItem(1 )); shopitem.Add(new ShopItem(4 )); shopitem.Sort(Sortshopitem); shopitem.Sort(delegate (ShopItem a, ShopItem b) { if (a.id > b.id) { return 1 ; } else return -1 ; return 0 ; }); shopitem.Sort((ShopItem a, ShopItem b) => { return a.id > b.id ? 1 : -1 ; }); static int Sortshopitem (ShopItem a, ShopItem b ){ if (a.id > b.id) { return 1 ; } else return -1 ; return 0 ; }
协变逆变 什么是协变逆变 协变:和谐的变化,自然的变化,因为里式替换原则中父类可以装子类,所以子类变父类比如string变成object感受起来是和谐的。
逆变:逆常规的变化,不正常的变化,因为里式替换原则中父类可以装子类,但是子类不能装父类,所以父类变子类如object变string感受是不和谐的。
协变和逆变是用来修饰泛型的;协变:out;逆变:in。
用于泛型中修饰泛型字母的,只有泛型接口和泛型委托能使用。
作用 返回值和参数 用out修饰的泛型,只能作为返回值。
1 delegate T Testout <out T >() ;
用in修饰的泛型,只能作为参数
1 delegate void Testin <in T >(T v ) ;
结合里式替换原则 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 delegate T Testout <out T >() ;delegate void Testin <in T >(T v ) ;class father { } class son : father { } Testout<son> os = () => { return new son(); }; Testout<father> of = os; father f = of(); Testin<father> IF = (v) => { }; Testin<son> IS = IF; IS(new son());
总结:协变就是父类装子类,逆变就是子类装父类。(泛型委托)
多线程 进程 进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 简单来说就是打开一个应用程序就是开启了一个进程,进程之间可以相互独立运行,互不干涉;进程之间也可以相互访问,操作。 打开电脑资源管理器我们就能看到一个个正在运行的前台进程和后台进程。
线程 操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,一条线程指的是进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,目前我们写的程序都在主线程中。 简单理解就是代码从上到下运行的一条管道。
多线程 我们可以通过代码开启新的线程,可以同时运行代码的多条管道就叫多线程。
语法 c#提供了一个名为thread的多线程关键字。
声明 1 2 3 4 5 6 7 8 using System.Threading;Thread t = new Thread(NewThreadLogic); static void NewThreadLogic (){ 新开线程执行的代码逻辑,在该函数语句块中。 }
启动线程
设置为后台线程 后台线程:当前台线程结束了的时候,整个进程也就结束了,即使还有后台线程正在运行,后台线程不会防止应用程序的进程被终止,如果不设置为后台进程可能导致进程无法正常关闭。
1 2 3 4 5 6 7 8 static void NewThreadLogic (){ 新开线程执行的代码逻辑,在该函数语句块中。 while (1 ) { } }
1 2 3 4 5 6 7 8 9 t.IsBackground = true ; static void NewThreadLogic (){ 新开线程执行的代码逻辑,在该函数语句块中。 while (1 ) { } }
关闭线程 如果开启的线程中不是死循环,是能够提前结束的逻辑,那么就不用刻意去关闭它,但是如果是死循环的话,就必须终止这个线程,这里有两种方法
死循环中bool标识
通过线程提供方法(注意在.Net core 版本中无法终止会报错)
1 2 3 4 5 6 7 8 9 10 11 12 static bool IS = true ;t.start(); Console.ReadKey(); IS = false ; static void NewThreadLogic (){ 新开线程执行的代码逻辑,在该函数语句块中。 while (IS) { } }
1 2 3 4 5 6 7 8 9 10 11 12 static bool IS = true ;t.start(); t.Abort(); t = null ; static void NewThreadLogic (){ 新开线程执行的代码逻辑,在该函数语句块中。 while (IS) { } }
线程休眠
线程之间共享数据 多个线程使用的内存是共享的,都是属于应用进程,所以要注意,当同时操作同一片内存区域可能出现问题,可以使用加锁的形式解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static bool IS = true ;t.start(); IS = false ; while (1 ){ Console.SetCursorPosition(0 ,0 ); } static void NewThreadLogic (){ 新开线程执行的代码逻辑,在该函数语句块中。 while (IS) { Console.SetCursorPosition(10 ,5 ); } }
此时有可能会出现问题。 使用关键字:lock就能解决。 当我们在多线程中想要访问同样的东西时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static bool IS = true ;object obj = new object ();t.start(); IS = false ; while (1 ){ lock (obj) { Console.SetCursorPosition(0 ,0 ); } } static void NewThreadLogic (){ 新开线程执行的代码逻辑,在该函数语句块中。 while (IS) { lock (obj) { Console.SetCursorPosition(10 ,5 ); } } }
多线程对我们的意义 可以用多线程实现处理一些复杂的耗时的问题,比如寻路、网络通信等。
预处理器指令 编译器 编译器是一种翻译程序,它用于将源语言程序翻译成目标语言程序。 源语言程序:某种程序设计语言写成的,比如c,c#,c++等等。 目标语言程序:二进制数表示的伪机器代码的程序。
预处理器指令 指导编译器在实际编译开始前对信息进行预处理。 预处理器指令都是以#开头 预处理器指令不是语句,不适用;结束。 目前我们使用的头文件就是和折叠代码块就是预处理器指令。
常见的预处理器指令 define 定义一个符号,类似一个没有值的变量。 #undef 取消define定义的符号,让其失效。 两者都是写在脚本文件最前面的,一般配合if使用。
if 与if语句规则一样,一般配合define使用。
1 2 3 4 5 6 7 #if Unity #elif Unity4 #else #endlf
warning和error 告诉编译器是警告还是报错。 一般配合if使用。
1 2 3 4 5 #if Unity #warning 警告 #error 错误 #endlf